In this article, we’ll explore what’s new in PHP 8.5. We’ll focus on some of the bigger changes, including closures in constant expressions, changes to the clone keyword, and URL parsing. Then, I’ll provide a summary of the smaller features in the language, with examples and explanations of the latest improvements. Let’s dive straight in.
PHP 8.5: Closures in Constant Expressions
The first thing that we’re going to look at is closures in constant expressions. It was never possible to provide a default value in an array that defines a set of closures, that you then can call in order. With PHP 8.5, it is now possible to define such a list, as seen in the example below:
<?php
function slugger(
string $input,
array $callbacks = [
static function ($value) { return \strtolower($value); },
static function ($value) { return \preg_replace('/[^a-z]/', '-', $value); },
static function ($value) { return \trim($value, '-'); },
static function ($value) { return \preg_replace('/-+/', '-', $value); },
]
) {
foreach ($callbacks as $callback) {
$input = $callback($input);
}
return $input;
}
?>
In this example, the default value of the $callbacks array contains four closures, that the foreach loop then loops over to call. A user of this function can also provide their own array of callbacks.
There are some restrictions here, because these need to be static calls. That means they can’t be methods called on objects using $this. They also cannot use any values from outside the scope that these are defined in. That means that you can’t use “use” here, or short closures starting with fn().
IPC NEWSLETTER
All news about PHP and web development
In addition to this, it is now also possible to use first-class callables. A first-class callable is just a form of closure, and these can also never have any information coming in from the outside scope, like in this example:
<?php
function slugger(
string $input,
array $callbacks = [
\strtolower(...),
static function ($value) { return \preg_replace('/[^a-z]/', '-', $value); },
]
) {
foreach ($callbacks as $callback) {
$input = $callback($input);
}
return $input;
}
?>
Here we have replaced the static function ($value) { return \strtolower($value); } call to \strtolower(…). This is still quite a clunky way of creating such a function, where your $input is transformed through multiple function calls. To alleviate this, PHP 8.5 also introduces a new feature to resolve all of this: the new pipe operator (|>).
The pipe operator is a way of chaining methods together, called in order, with a value passed along between them. You can then also compose some interesting functions. With this, we can rewrite our slugger method to:
<?php
function slugger(string $input)
{
return $input
|> \strtolower(...)
|> (fn($x) => \preg_replace('/[^a-z]/', '-', $x))
|> (fn($x) => \trim($x, '-'))
|> (fn($x) => \preg_replace('/-+/', '-', $x));
}
?>
Like in the earlier examples, the slugger method takes the $input string, and then passes the input to strtolower(…), a short closure. Afterwards, it passes the result value of thhat to preg_replace(), trim(), and preg_replace() again.
However, because these functions take more than one argument, you can’t directly use the first class callable here. Pipes can only pass one value to the next call in the pipeline.
At the moment, to go around that, you have to wrap a short closure around the functions that would normally take more than one argument. The whole closure definition should also be wrapped again in parenthesis to avoid issues with priorities in the PHP code parser. That is why the example uses:
(fn($x) => \preg_replace('/[^a-z]/', '-', $x)).
This allows you to pre-define the other arguments to these functions. It uses the pipe operator that pipes the left-hand side to the closure, defined with fn($x) here, which then gets passed by the pipe operator to the third argument of the preg_replace() call as $x too.
Maybe in PHP 8.6 or later, there will be a better way of doing this, through a newly suggested feature called Partially Applied Functions.
With the pipe operator you have no insight to the value that gets passed from function to function or closure. This makes debugging a lot harder at first sight. With the first implementation, there was no way to get to this value, but through some changes in PHP, it is now possible for debuggers like Xdebug to see and present the intermediate stages of the pipe chain without you having to assign the intermediate value to a variable.
Clone With in PHP 8.5
The next feature we’re going to look at is changes to the clone keyword. For a while, PHP has had read-only and final classes, which tend to be used as value objects to be passed around. Value objects are meant to be read-only and unmutable, but sometimes you might want to update these value objects to replace certain properties with new values.
Up until now, you couldn’t really do that without resulting to a hack by creating a wither method like:
<?php
final readonly class Response {
public function __construct(
public int $statusCode,
public string $reasonPhrase,
) {}
public function withStatus($code, $reasonPhrase = ''): Response
{
$values = get_object_vars($this);
$values['statusCode'] = $code;
$values['reasonPhrase'] = $reasonPhrase;
return new self(
...$values
);
}
}
?>
This only works in some situations, because this style of implementation relies on all the arguments being settable ad named arguments via the constructor when new self(…$values) is called.
The PHP development team originally wanted to create a specific new syntax addition to clone, to allow a new value object to be created with some properties modified. But a totally new syntax would complicate matters, as users, static analysis tools, and other tools would have to support it. Instead of a new dedicated syntax, the PHP developers have changed the clone keyword into a language construct/function hybrid.
This is needed, because up to now, a clone was a language construct only. This means that it was not possible to have arguments, as language constructs in PHP don’t really support that. It can only have a single expression as its right-hand-value.
The new feature in PHP 8.5 extends clone to make it into a function, which accepts two arguments. The first one being the object to clone, and the second one an array of property names and their new values:
<?php
final readonly class Response {
public function __construct(
public int $statusCode,
public string $reasonPhrase,
) {}
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone($this, [
"statusCode" => $code,
"reasonPhrase" => $reasonPhrase,
]);
}
}
?>
The withStatus() method here accepts a $code argument, and an optional $reasonPhrase argument. The clone first creates a new Response object, with all the properties set to the values of the original object.
For each of the elements in the array passed as second argument, their values are going to be set on each property with the same name (statusCode and reasonPhrase), and in the same order as how they are present in the array.
Because these are internally just a normal assignment operation, it also means that each internal assignment will follow all the requirements for the values of these properties, including type checks, and visibility checks. Property hooks and __set methods are also called as with normal assignments. The only restriction that is lifted, is the “write-once” property of readonly properties.
URL Parsing in PHP 8.5
The third big feature that we’re going to look at is URL parsing. For a long time, PHP has had the parse_url() function, which takes a URL or URI and parses this into its components. However, this function doesn’t follow any standard, has some strange PHP-isms while parsing the URL, and in general isn’t very useful for parsing URLs according to any standard, or using them safely in the modern web.
PHP 8.5 improves on this situation by introducing two new classes to parse, represent, and modify URLs. Each of the two variants is slightly different because they follow a slightly different standard. You can construct either of these by using “new”, but there is also a static parse() method. The constructor approach will throw an Uri\InvalidUriException when it encounters an invalid URI. The parse() factory method does not do this, and instead returns null.
The first one that we introduced is the Uri\WhatWg\Url class. Both the constructor and the parse() factory method parse the URL according to the WhatWG standard. Once parsed, you can access each of the component parts, create a new object with a component in the URL changed through a wither method, and then retrieve a fully assembled URL as a string again.
This class is best used if you need to do something with URLs that you’re going to embed into HTML. For example, when you regenerate URLs in a CMS, etc. Beyond the WhatWgUrl class, there is also the \Uri\Rfc3986\Url class. This parses the URL according to slightly different standards, in this case the RFC3986 standard.
This kind of URL is mostly used for server-to-server communication. Think of it as DSN parsing or outgoing HTTP requests that you make yourself. Both classes implement very similar methods that are not quite the same, because the concepts for each of these two different URL types are distinct.
Let’s have a look at our first one. In this example, we’re showing how to use the new Uri\WhatWg\Url class to parse our example URL. With the methods getScheme(), getAsciiHost(), getPath(), getQuery() and getFragment(), we can then get access to the original constituent parts:
<?php
// Parse URL:
$url = new \Uri\WhatWg\Url('https://friday-night-dinners.co.uk/archive/?search=local#artean');
// Show components:
echo $url->getScheme(), "\n";
echo $url->getAsciiHost(), "\n";
echo $url->getPath(), "\n";
echo $url->getQuery(), "\n";
echo $url->getFragment(), "\n";
?>
This outputs:
https
friday-night-dinners.co.uk
/archive
search=local
artean
It is also possible to modify these parts by calling wither methods as well. We continue from the previous example with:
<?php
$newUrl =
$url->withPath('/latest')
->withQuery('search=spanish')
->withFragment('');
?>
Please note that you need to assign the result from the wither methods to a new variable. The object is immutable and a new object will be returned from each of these methods. With the URL modified, we can finally convert it back to a full string:
<?php
echo $newUrl->toAsciiString();
?>
Which then outputs:
https://friday-night-dinners.co.uk/latest?search=spanish
Both the WhatWg\Url and Rfc3986\Url classes will know how to adapt the specific components according to the respective specification correctly. This also ensures that the strings that WhatWg\Url::toAsciiString() method, and its counterpart Rfc3986\Url::toString(), produce, are correctly formed as well.
Other Features in PHP 8.5
Now let’s see some of the smaller features that have been added in PHP 8.5.
Final Constructor Property Promotions
Constructor property promotions were introduced in PHP 8.1. These allow you to specify the visibility of a typed property inside the constructor’s argument definition, instead of having to define them separately, and then do the assignments from arguments to these properties manually in the constructor.
In PHP 8.4, we introduced property hooks that allow you to run some code when a property is being get or set with a user-defined function. With the inclusion of this, PHP also gained final properties, but these properties were not allowed to be defined in a constructor for property promotion.
PHP 8.5 now adds this functionality, as you can see in the following example:
<?php
class User
{
public function __construct(
final private string $first,
final private string $last,
) {}
final public string $fullName {
get => $this->first . " " . $this->last;
set { [$this->first, $this->last] = explode(' ', $value); }
}
}
$u = new User("Derek", "Rethans");
$u->fullName = "Derick Rethans";
echo $u->fullName, "\n";
?>
Extending the User class and redefining the type of the final private string properties $first and $last are now prohibited.
The #[noDiscard] Attribute
This new attribute enforced that during run time, the calling function consumes the returned value (by assignment, or it being passed on to another function as argument).
For example, if you have a DateTimeImmutable class and call the setDate method on it, you also will have to assign it to a new variable, otherwise the modification disappears. This is because DateTimeImmutable’s set methods return a new object and don’t modify the original one. Just like the two URL classes from earlier through their wither methods.
In PHP 8.5, the methods on the DateTimeImmutable class that return a new object now have this new NoDiscard attribute attached to them. When you don’t assign the return value to a new variable, you will get a run-time warning, like in this example:
<?php
$dt = new DateTimeImmutable();
$dt->setTime(9, 45);
?>
It will show you this warning to hint that you need to assign the newly created object to a variable:
Warning: The return value of method DateTimeImmutable::setTime() should either be used or intentionally ignored by casting it as (void), as DateTimeImmutable::setTime() does not modify the object itself.
As the message indicates, you can ignore the returned value by using the (void) cast, but at least with DateTimeImmutable, this makes no sense. You can use the #[NoDiscard] attribute in code that you write as well. It is an additional helper to make sure that you, or your library’s users, are not making mistakes in their code.
Note: The new WhatWg\Url and Rfc3986\Url classes have with* methods. This naming convention already signals that these return a new object, which is not something that is apparent with the set*-named methods from the DateTimeImmutable class. Because of this, the Url classes do not have the #[NoDiscard] attribute attached to them at the time of writing.
IPC NEWSLETTER
All news about PHP and web development
Filter Extension
The filter extension has a new mode for when you validate incoming request variables. Normally, the filter_var() function would return false if it couldn’t validate the value. With an option flag to filter_var() you can make it instead return null if the input variable didn’t match with what you expected.
There is already a flag, FILTER_NULL_ON_FAILURE to make it return null in these situations, instead of false. This is useful because some probably values indeed run boolean false as a valid value. However, even with the FILTER_NULL_ON_FAILURE flag enabled, it makes for interesting and complex code. Instead, it is much better to be able to catch an exception.
PHP 8.5 introduces the FILTER_THROW_ON_FAILURE mode for filter_var(), which means that if an error is encountered while filtering the value to make sure it is correct, it will throw an exception, which you can then catch and handle in one go.
As you can see in this example here:
<?php
function validateUser(string $email, string $userId, string $userName) : bool
{
try {
filter_var($email, FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE);
filter_var($userId, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE);
filter_var(
$userName, FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_THROW_ON_FAILURE]
);
return true;
} catch (\Filter\FilterFailedException $e) {
return false;
}
}
?>
array_first() and array_last()
PHP 7.3 introduced the array_key_first() and array_key_last() functions, to get either the first or the last key from an array. At that time, we didn’t introduce any functions to obtain the first and last array element values, because we weren’t quite sure whether we needed that.
However, it has now become clear that these are actually useful. This is why in PHP 8.5, we now have two new functions: array_first() and array_last(). These respectively return the first or last element values from an array, as I show you in this example here:
<?php
$timezone = new DateTimeZone("Europe/Kyiv");
$trans = $timezone->getTransitions();
var_dump(array_first($trans), array_last($trans));
?>
OPcache Built-In
The last new change in PHP 8.5 that I want to focus on is that the OPcache extension can no longer be disabled. It is no longer a shared extension that you need to load specifically into PHP, and instead, it is built-in as a static extension like ext/standard or ext/date.
Although it is built-in, it does not mean that OPcache is also enabled by default. You still need to make the correct configuration settings to do so. Due to this tighter coupling, the PHP development team has now more freedom to utilise features in OPcache, such as its optimiser, in a more coherent fashion. It likely opens up avenues to improve performance, which is something that the PHP development team can now investigate.
Conclusion
The new features as presented here, are a high level overview of some of the bigger improvements and additions.
To see the full list, please visit our release page at https://www.php.net/releases/8.5/en.php, and the full change log at https://www.php.net/ChangeLog-8.php#PHP_8_5.





